Data Wrangling(整理版)
原始资料: Missing Semester - Data Wrangling created: 2026-07-01 11:19 整理说明: 本版本结合原始笔记和可读取的原始教程重排、补全和翻译,保留常用英文术语;原始笔记未修改。
内容简要概括
Data Wrangling 指把原始数据整理成更适合阅读、统计或继续处理的形式,最常见的方式是用 shell pipeline 把多个小工具组合起来。本文以 SSH 日志为例,串联 ssh、grep、sed、sort、uniq、awk、paste 和 xargs,展示如何从大量日志中抽取用户名、统计频次并生成参数列表。核心思路是:先过滤,再抽取,再排序统计,最后按需要格式化输出或交给下一个命令执行。
Data Wrangling、pipeline、grep、sed、awk、regular expression、capture group、sort、uniq、paste、xargs、journalctl、ssh log
目录
- 1. grep、sed、awk 常用方式与参数速查
- 2. Data Wrangling 的基本思路
- 3. 从远程 SSH 日志开始过滤
- 4. sed 与 regular expression
- 5. 提取用户名并统计频次
- 6. awk 处理字段与条件
- 7. Data Wrangling to Make Arguments
- 8. Wrangling Binary Data
- 9. 可复用命令模板汇总
1. grep、sed、awk 常用方式与参数速查
1.1 grep: 按行过滤文本
grep 用来从输入中筛选匹配某个 pattern 的行。它最适合做 pipeline 的早期过滤,把大量文本先缩小到相关行。
常用形式:
grep PATTERN file
command | grep PATTERN
grep -i "error" app.log
grep -v "debug" app.log
grep -E "error|warning" app.log
grep -F "[literal text]" app.log
常用参数:
| 参数 | 含义 | 典型用途 |
|---|---|---|
-i |
ignore case,忽略大小写 | 同时匹配 Error、error、ERROR |
-v |
invert match,反向匹配 | 排除包含某个 pattern 的行 |
-E |
extended regexp,启用扩展正则 | 使用 +、?、` |
-F |
fixed string,把 pattern 当普通字符串 | 不希望 .、[ 等字符被当成正则 |
-n |
显示行号 | 定位配置或日志位置 |
-c |
只输出匹配行数 | 快速统计 |
-r / -R |
递归搜索目录 | 在项目目录中查找文本 |
-A N |
after,显示匹配行之后 N 行 | 查看日志上下文 |
-B N |
before,显示匹配行之前 N 行 | 查看错误前发生了什么 |
-C N |
context,显示前后各 N 行 | 同时查看上下文 |
常见组合:
# 找出 sshd 相关日志
journalctl | grep sshd
# 找出包含 Disconnected from 的 sshd 日志
journalctl | grep sshd | grep "Disconnected from"
# 排除某类 nightly toolchain
rustup toolchain list | grep nightly | grep -vE "nightly-x86"
1.2 sed: 流式替换、删除与抽取
sed 是 stream editor,擅长对每一行文本执行替换、删除、打印等操作。最常见命令是 s/REGEX/SUBSTITUTION/,即把匹配 REGEX 的部分替换成 SUBSTITUTION。
常用形式:
sed 's/old/new/' file
command | sed 's/old/new/g'
command | sed -E 's/(user|admin): (.*)/\2/'
sed -n '1,20p' file
sed '/DEBUG/d' file
常用参数与命令:
| 写法 | 含义 | 典型用途 |
|---|---|---|
s/A/B/ |
每行只替换第一个 A 为 B |
简单替换 |
s/A/B/g |
每行替换所有 A 为 B |
全局替换 |
s/A// |
每行删除第一个 A |
删除匹配文本 |
s/A//g |
每行删除所有 A |
批量清理 |
-E |
使用 extended regular expression | 少写反斜杠,便于使用 ()、?、+ |
-n |
suppress automatic printing | 搭配 p 只打印指定内容 |
p |
print,打印匹配或指定行 | sed -n '1,10p' file |
d |
delete,删除匹配行 | sed '/DEBUG/d' file |
-i |
in-place edit,原地修改文件 | 修改前要确认备份策略 |
sed 的替换命令可以用捕获组保留想要的部分:
echo "user: alice" | sed -E 's/user: (.*)/\1/'
输出:
alice
1.3 awk: 按字段处理文本流
awk 是一门适合处理文本流的小型编程语言。它默认按空白字符把每一行切成字段,$0 表示整行,$1、$2、... 表示第 1、第 2 个字段。
常用形式:
awk '{print $2}' file
command | awk '{print $1, $2}'
command | awk -F, '{print $1}'
command | awk '$1 > 10 {print $2}'
command | awk 'BEGIN {sum = 0} {sum += $1} END {print sum}'
常用参数与内置变量:
| 写法 | 含义 | 典型用途 |
|---|---|---|
'{print $2}' |
打印每行第 2 个字段 | 从统计结果中取用户名 |
-F SEP |
设置 field separator | 处理 CSV、冒号分隔日志等 |
$0 |
当前整行 | 打印或匹配完整行 |
$1, $2 |
当前行的第 1、第 2 个字段 | 提取列 |
NF |
number of fields | 获取字段数,$NF 是最后一列 |
NR |
number of records | 当前行号 |
BEGIN |
输入开始前执行 | 初始化变量或打印表头 |
END |
输入结束后执行 | 输出累计结果 |
~ /REGEX/ |
字段匹配正则 | awk '$2 ~ /^c.*e$/' |
&&, ` |
` |
awk 的基本结构是:
pattern { action }
如果省略 pattern,默认匹配所有行;如果省略 action,默认打印整行。
2. Data Wrangling 的基本思路
Data Wrangling 的核心不是某一个工具,而是把许多小工具串成 pipeline:
- 用
grep先筛掉无关数据。 - 用
sed或awk抽取需要的字段。 - 用
sort、uniq、wc等工具做统计。 - 用
paste、xargs或其他命令把结果变成后续命令需要的格式。
只要使用了 | 把一个命令的输出交给另一个命令继续处理,就已经在做某种形式的 Data Wrangling。例如:
journalctl | grep -i intel
这条命令把完整系统日志转换成“只包含 intel 的日志行”。看起来只是搜索,实质上已经把原始数据变成了更有用的子集。
3. 从远程 SSH 日志开始过滤
3.1 在本地过滤远程日志
下面这条命令在远程服务器执行 journalctl,把完整日志传回本地,再在本地用 grep 过滤:
ssh myserver journalctl | grep sshd
问题是远程日志可能非常大。这样会把所有日志都传回本地,即使最后只需要其中很少一部分,网络传输和本地处理都会比较慢。
3.2 在远程先过滤,再传回本地
更好的方式是在远程服务器上完成主要过滤,再把结果传回本地:
ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' | less
这里的单引号很重要:被引号包住的整段 pipeline 会在远程服务器执行。这样本地只接收已经过滤过的日志,再用 less 分页查看。
命令拆解:
| 片段 | 作用 |
|---|---|
ssh myserver '...' |
登录远程服务器并执行引号内的命令 |
journalctl |
读取系统日志 |
grep sshd |
保留 SSH daemon 相关日志 |
grep "Disconnected from" |
进一步保留断开连接相关日志 |
less |
本地分页查看长输出 |
3.3 保存过滤结果,方便本地调试
调试 pipeline 时,可以先把远程过滤结果保存成文件,避免每次都访问网络:
ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' > ssh.log
less ssh.log
> 表示重定向输出,把命令结果写入 ssh.log。后续可以直接对 ssh.log 做 sed、awk、sort 等实验。
4. sed 与 regular expression
4.1 删除固定前缀
原始日志中有很多前缀信息。如果只想保留 Disconnected from 后面的内容,可以写:
ssh myserver journalctl \
| grep sshd \
| grep "Disconnected from" \
| sed 's/.*Disconnected from //'
sed 's/.*Disconnected from //' 的含义是:
| 部分 | 含义 |
|---|---|
s/.../.../ |
substitution,替换命令 |
.* |
任意字符重复 0 次或多次 |
Disconnected from |
要匹配的字面文本 |
第二个 / 后为空 |
替换为空,也就是删除匹配内容 |
因此,它会把每行中 Disconnected from 及其之前的内容删掉,只留下后面的部分。
4.2 常见 regular expression 元字符
| 写法 | 含义 |
|---|---|
. |
匹配除 newline 以外的任意单个字符 |
* |
前一个匹配重复 0 次或多次 |
+ |
前一个匹配重复 1 次或多次 |
[abc] |
匹配 a、b、c 中任意一个字符 |
[^ ] |
匹配任意非空格字符 |
| `(RX1 | RX2)` |
? |
前一个匹配出现 0 次或 1 次 |
^ |
行首 |
$ |
行尾 |
[0-9]+ |
一个或多个数字 |
sed 默认使用 basic regular expression,部分元字符需要反斜杠才能生效。加上 -E 后使用 extended regular expression,写复杂正则会更自然。
4.3 贪婪匹配与 perl 的非贪婪写法
* 和 + 默认是 greedy,也就是尽可能多地匹配文本。比如用户名本身叫 Disconnected from 时:
Jan 17 03:13:00 example.com sshd[2631]: Disconnected from invalid user Disconnected from 46.97.239.16 port 55920 [preauth]
简单写法:
sed 's/.*Disconnected from //'
可能会匹配到最后一个 Disconnected from,导致用户名被吃掉,只剩下 IP 和端口。某些正则引擎支持 .*? 表示非贪婪匹配,但 sed 不支持这种写法。可以用 perl:
perl -pe 's/.*?Disconnected from //'
不过本教程继续使用更常见的 sed,通过匹配整行和捕获组来解决问题。
4.4 匹配整行并删除整行内容
为了避免用户名中的 Disconnected from 干扰,可以匹配完整日志结构:
sed -E 's/.*Disconnected from (invalid |authenticating )?user .* [^ ]+ port [0-9]+( \[preauth\])?$//'
正则拆解:
| 片段 | 含义 |
|---|---|
.*Disconnected from |
匹配日志开头直到 Disconnected from |
| `(invalid | authenticating )?` |
user |
匹配字面文本 user |
.* |
匹配用户名部分 |
[^ ]+ |
匹配一个空格后的一段非空格文本,通常是 IP |
port [0-9]+ |
匹配端口字段 |
( \[preauth\])? |
可选匹配 [preauth],方括号需要转义 |
$ |
匹配行尾 |
| 替换为空 | 删除整行匹配内容 |
这个版本能识别整条日志,但因为替换结果为空,所以会把匹配到的行变成空行。
4.5 使用捕获组提取用户名
真正需要的是保留用户名。把用户名部分放进捕获组,再在替换结果中引用它:
sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
关键点:
| 片段 | 含义 |
|---|---|
| `(invalid | authenticating )?` |
(.*) |
第 2 个捕获组,保存用户名 |
\2 |
在替换结果中引用第 2 个捕获组 |
完整 pipeline:
ssh myserver journalctl \
| grep sshd \
| grep "Disconnected from" \
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
输出会变成一行一个用户名,后续就可以继续排序和统计。
5. 提取用户名并统计频次
5.1 sort 与 uniq -c
先得到用户名列表,再统计每个用户名出现了多少次:
ssh myserver journalctl \
| grep sshd \
| grep "Disconnected from" \
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' \
| sort \
| uniq -c
sort 会把输入按字典序排序。uniq -c 会合并相邻的重复行,并在前面加上出现次数。
必须先 sort 的原因是:uniq 只合并相邻重复行。例如:
root
admin
root
直接 uniq -c 无法把两个 root 合并。排序后:
admin
root
root
再运行 uniq -c,相同用户名就相邻了。
5.2 按次数排序并取前 10 个
ssh myserver journalctl \
| grep sshd \
| grep "Disconnected from" \
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' \
| sort \
| uniq -c \
| sort -nk1,1 \
| tail -n10
命令拆解:
| 片段 | 含义 |
|---|---|
sort |
把相同用户名排到一起 |
uniq -c |
统计每个用户名出现次数 |
sort -n |
按数字大小排序,而不是按字符串排序 |
sort -k1,1 |
只使用第 1 列作为排序键 |
sort -nk1,1 |
-n 和 -k1,1 的组合写法 |
tail -n10 |
取最后 10 行,即出现次数最多的 10 个 |
如果想看最少见的用户名,可以用 head 代替 tail。如果想降序排序,可以使用 sort -r。
5.3 生成逗号分隔的用户名列表
有时需要把一行一个用户名转换成逗号分隔的列表,例如写入配置文件:
ssh myserver journalctl \
| grep sshd \
| grep "Disconnected from" \
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' \
| sort \
| uniq -c \
| sort -nk1,1 \
| tail -n10 \
| awk '{print $2}' \
| paste -sd,
命令拆解:
| 片段 | 含义 |
|---|---|
awk '{print $2}' |
从 uniq -c 的结果中取第 2 列,也就是用户名 |
paste -s |
serial,把多行合并成一行 |
paste -d, |
delimiter 使用逗号 |
paste -sd, |
合并成一行,并用逗号分隔 |
在 macOS 上,系统自带的是 BSD paste,和 GNU paste 在某些行为上可能不同。如果遇到兼容问题,可以安装 GNU coreutils 或改用其他方式生成列表。
6. awk 处理字段与条件
6.1 打印指定字段
awk '{print $2}' 的意思是:对每一行,打印第 2 个字段。
在下面这种输入中:
12 root
3 admin
1 test
命令:
awk '{print $2}'
输出:
root
admin
test
字段默认由空白字符分隔。如果数据是逗号分隔,可以用 -F,:
awk -F, '{print $2}' users.csv
6.2 使用 pattern 过滤行
awk 程序可以写成 pattern { action }。例如统计只出现 1 次、并且用户名以 c 开头、以 e 结尾的数量:
awk '$1 == 1 && $2 ~ /^c[^ ]*e$/ { print $2 }' | wc -l
拆解:
| 片段 | 含义 |
|---|---|
$1 == 1 |
第 1 列等于 1,也就是出现次数为 1 |
&& |
逻辑与,两个条件都要满足 |
$2 ~ /^c[^ ]*e$/ |
第 2 列匹配正则:以 c 开头,以 e 结尾 |
{ print $2 } |
打印第 2 列用户名 |
wc -l |
统计输出行数 |
6.3 使用 BEGIN 和 END 做累计
awk 不只是字段抽取工具,也可以保存变量并做简单计算:
BEGIN { rows = 0 }
$1 == 1 && $2 ~ /^c[^ ]*e$/ { rows += $1 }
END { print rows }
含义:
| 部分 | 作用 |
|---|---|
BEGIN { rows = 0 } |
读取输入前初始化变量 |
| 条件块 | 对满足条件的行,把第 1 列累加到 rows |
END { print rows } |
输入结束后输出累计结果 |
理论上,awk 可以替代前面部分 grep 和 sed 的工作,但拆成多个小工具通常更容易学习、调试和解释。
7. Data Wrangling to Make Arguments
数据整理工具配合 xargs 很有用,因为 xargs 可以把前面 pipeline 的输出变成后面命令的参数。
示例:卸载旧的 Rust nightly toolchain。
rustup toolchain list \
| grep nightly \
| grep -vE "nightly-x86" \
| sed 's/-x86.*//' \
| xargs rustup toolchain uninstall
命令拆解:
| 片段 | 含义 |
|---|---|
rustup toolchain list |
列出已安装的 Rust toolchain |
grep nightly |
保留 nightly 版本 |
grep -vE "nightly-x86" |
排除当前想保留的 nightly-x86 相关版本 |
sed 's/-x86.*//' |
删除 -x86 及其后面的部分,只留下 toolchain 名称前缀 |
xargs rustup toolchain uninstall |
把输入中的 toolchain 名称作为参数传给卸载命令 |
xargs 常见参数:
| 参数 | 含义 | 典型用途 |
|---|---|---|
-n N |
每次传 N 个参数给后续命令 | 控制批处理粒度 |
-I {} |
用 {} 作为占位符 |
把输入插入命令中间 |
-0 |
使用 null 字符作为分隔符 | 搭配 find -print0 安全处理带空格文件名 |
-r |
没有输入时不执行命令 | GNU xargs 常用,避免误执行 |
涉及删除、卸载、移动文件等操作时,建议先把 xargs 后面的命令换成 echo 检查参数:
rustup toolchain list \
| grep nightly \
| grep -vE "nightly-x86" \
| sed 's/-x86.*//' \
| xargs echo rustup toolchain uninstall
确认输出无误后,再执行真正的卸载命令。
8. Wrangling Binary Data
前面主要处理文本数据,但 pipeline 对 binary data 也同样有用。原始教程给出的例子是:从摄像头抓取一帧图像,转成灰度,压缩,通过 SSH 传到远程机器,在远程解压、保存副本并显示。
ffmpeg -loglevel panic -i /dev/video0 -frames 1 -f image2 - \
| convert - -colorspace gray - \
| gzip \
| ssh mymachine 'gzip -d | tee copy.jpg | env DISPLAY=:0 feh -'
命令拆解:
| 片段 | 含义 |
|---|---|
ffmpeg ... -f image2 - |
从摄像头读取一帧图像,并把图像写到 stdout |
convert - -colorspace gray - |
从 stdin 读取图像,转换为灰度,再写到 stdout |
gzip |
压缩二进制数据流 |
ssh mymachine '...' |
把压缩后的数据流传到远程机器处理 |
gzip -d |
在远程解压 |
tee copy.jpg |
保存一份副本,同时继续把数据传给后续命令 |
env DISPLAY=:0 feh - |
在远程图形界面中显示图像 |
这说明 shell pipeline 不只适用于文本;只要命令能从 stdin 读取、向 stdout 写入,就可以组合处理文本或二进制流。
9. 可复用命令模板汇总
9.1 远程过滤日志并本地查看
ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' | less
9.2 保存远程过滤结果
ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' > ssh.log
less ssh.log
9.3 从 SSH 日志提取用户名
ssh myserver journalctl \
| grep sshd \
| grep "Disconnected from" \
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
9.4 统计出现最多的 10 个用户名
ssh myserver journalctl \
| grep sshd \
| grep "Disconnected from" \
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' \
| sort \
| uniq -c \
| sort -nk1,1 \
| tail -n10
9.5 输出逗号分隔的用户名列表
ssh myserver journalctl \
| grep sshd \
| grep "Disconnected from" \
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/' \
| sort \
| uniq -c \
| sort -nk1,1 \
| tail -n10 \
| awk '{print $2}' \
| paste -sd,
9.6 用 xargs 把整理结果变成命令参数
rustup toolchain list \
| grep nightly \
| grep -vE "nightly-x86" \
| sed 's/-x86.*//' \
| xargs rustup toolchain uninstall